import Point from '../utils/Point.js';
import * as validate from '../utils/validate.js';
import * as Timer from '../utils/Timer.js';
import * as Expert from '../utils/Expert.js';
import * as SYS from '../core/SYSTEM.enum.js';

// =========== base utils
export function objects2array(o) {
    let a = [];
    if (!o) return a;
    if (validate.isArray(o) && o.length) {
        o.forEach(v => {
            if (validate.isObject(v)) {
                let l = [];
                Object.keys(v).forEach(k => {
                    if (!v.hasOwnProperty(k)) return;
                    l.push(v[k]);
                });
                a.push(l);
            } else if (validate.isArray(v)) a.push(...[v]);
            else a.push(v);
        });
    } else if (validate.isObject(o) && Object.keys(o).length) {
        Object.keys(o).forEach(k => {
            if (!o.hasOwnProperty(k)) return;
            a.push([k, o[k]]);
        });
    } else if (!validate.isArray(o) && !validate.isObject(o)) {
        a = o;
    }
    return a;
}

export function array2object(a, columns = []) {
    let o = {};
    if (!a || !a.length) return o;
    [].forEach.call([].concat(a), v => {
        if (v.length === 2 && columns.length === 2 && v[0]) {
            o[v[0]] = v[1];
        } else if (Object.prototype.toString.call(v) === '[object Array]' && columns.length === v.length) {
            v.forEach((k, i) => {
                if (i === 0 && k) o[k] = {};
                else if (o[v[0]]) o[v[0]][columns[i]] = k;
            });
        }
    });
    return o;
}

export function array2objectArray(a, columns = []) {
    let rtn = [];
    if (!a || !a.length) return rtn;
    [].forEach.call([].concat(a), v => {
        if (Object.prototype.toString.call(v) === '[object Array]' && columns.length === v.length) {
            let o = {};
            v.forEach((k, i) => {
                o[columns[i]] = k;
            });
            rtn.push(o);
        }
    });
    return rtn;
}

export function shake(o) {
    let c = _.cloneDeep(o);
    Object.keys(c).forEach(k => {
        if (/Ext$/.test(k)) delete c[k];
    });
    return c;
}

export function combineObjects(list, o, property) {
    if (!list && !list.length) return o;
    o[property] = {};
    list.forEach(l => {
        if (!l.hasOwnProperty(property)) return;
        Object.keys(l[property]).forEach(k => {
            if (!l[property].hasOwnProperty(k)) return;
            if (!o[property].hasOwnProperty(k)) o[property][k] = l[property][k];
            else if (o[property][k] !== l[property][k]) o[property][k] = '?';
        });
    });
    list.forEach(l => {
        if (!l.hasOwnProperty(property)) return;
        Object.keys(o[property]).forEach(k => {
            if (!o[property].hasOwnProperty(k)) return;
            if (!l[property].hasOwnProperty(k)) o[property][k] = '?';
        });
    });
    return o;
}

export function combineArray(list, o, property) {
    if (!list && !list.length) return o;
    o[property] = [];
    list.forEach(l => {
        if (!l.hasOwnProperty(property) || !l[property].length) return;
        l[property].forEach((r, ri) => {
            if (typeof r === 'object') {
                if (!o[property][ri]) o[property][ri] = {};
                Object.keys(r).forEach(k => {
                    if (!o[property][ri].hasOwnProperty(k)) o[property][ri][k] = r[k];
                    else if (o[property][ri][k] !== r[k]) o[property][ri][k] = '?';
                });
            } else {
                if (!o[property][ri]) o[property][ri] = r;
                else if (o[property][ri] !== r) o[property][ri] = '?';
            }
        });
    });
    list.forEach(l => {
        if (!l.hasOwnProperty(property) || !l[property].length) return;
        o[property].forEach((r, ri) => {
            if (typeof r === 'object') {
                Object.keys(r).forEach(k => {
                    if (!l[property][ri] || l[property][ri][k] !== r[k]) r[k] = '?';
                    // if (!l[property][ri] || !l[property][ri][k] || l[property][ri][k] !== r[k]) r[k] = '?';
                });
            } else {
                if (!l[property][ri] || !l[property][ri] || l[property][ri] !== r) r = '?';
            }
        });
    });
    return o;
}

export function separateObject(obj, o, property) {
    if (!obj || !obj.hasOwnProperty(property)) return;
    Object.keys(obj[property]).forEach(k => {
        if (!obj[property].hasOwnProperty(k)) return;
        if (obj[property][k] !== '?') o[property][k] = obj[property][k];
    });
    Object.keys(o[property]).forEach(k => {
        if (!o[property].hasOwnProperty(k)) return;
        if (!obj[property][k]) delete o[property][k];
    });
    return o;
}

export function separateArray(obj, o, property) {
    if (!obj || !obj.hasOwnProperty(property)) return;
    obj[property].forEach((v, i) => {
        if (typeof v === 'object') {
            Object.keys(v).forEach(k => {
                if (v[k] !== '?') o[property][i][k] = v[k];
            });
        } else {
            if (v !== '?') o[property][i] = v;
        }
    });
    return o;
}

// =========== project utils

// users
export function extendUsers(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendUser(o));
    return list;
}

export function extendUser(o) {
    if (!o) return;
    o.initTimeExt = o.initTime ? Timer.specialTime(o.initTime) : '';
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.subscriptionsExt = 'icon icon-subtract';
    o.accessExt = SYS.USER_ROLES.CUSTOM;
    o.accessListExt = [];
    o.lastRequestTimeExt = Timer.specialTime(o.lastRequestTime);
    o.remoteExt = '';
    o.lockExt = 'icon icon-unlock';

    Object.assign(o, extendUserAsync(o));
    return o;
}

export function extendUserAsync(o, users, vehicles, subscriptions = []) {
    if (!o.id || !o.properties) return o;
    if (users && users.length) {
        let t = users.filter(v => v.id === o.id);
        if (!t || !t[0]) return o;
        o.subscriptions = _.cloneDeep(t[0].subscriptions);
        o.properties = t[0].properties;
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    if (o.subscriptions && o.subscriptions.length) o.subscriptionsExt = 'icon icon-user blue';
    o.accessExt = getUserRole(o.access, subscriptions);
    o.lastRequestTimeExt = Timer.specialTime(o.lastRequestTime);
    if (vehicles && vehicles.length) {
        let t = vehicles.find(v => v.remoteId === o.id);
        o.remoteExt = t && t.id ? t.id : '';
    }
    return o;
}

export function access2list(access, subscriptions) {
    let subs = [];
    subscriptions.forEach(v => {
        subs.push([
            v,
            access[SYS.HTTP_METHODS.GET] ? access[SYS.HTTP_METHODS.GET].indexOf(v) !== -1 : false,
            access[SYS.HTTP_METHODS.PUT] ? access[SYS.HTTP_METHODS.PUT].indexOf(v) !== -1 : false,
            access[SYS.HTTP_METHODS.POST] ? access[SYS.HTTP_METHODS.POST].indexOf(v) !== -1 : false,
            access[SYS.HTTP_METHODS.DELETE] ? access[SYS.HTTP_METHODS.DELETE].indexOf(v) !== -1 : false,
            access[SYS.HTTP_METHODS.WS] ? access[SYS.HTTP_METHODS.WS].indexOf(v) !== -1 : false
        ]);
    });
    return subs;
}

export function list2access(list) {
    let access = {
        [SYS.HTTP_METHODS.GET]: [],
        [SYS.HTTP_METHODS.PUT]: [],
        [SYS.HTTP_METHODS.POST]: [],
        [SYS.HTTP_METHODS.DELETE]: [],
        [SYS.HTTP_METHODS.WS]: []
    };
    list.forEach(v => {
        if (v[1]) access[SYS.HTTP_METHODS.GET].push(v[0]);
        if (v[2]) access[SYS.HTTP_METHODS.PUT].push(v[0]);
        if (v[3]) access[SYS.HTTP_METHODS.POST].push(v[0]);
        if (v[4]) access[SYS.HTTP_METHODS.DELETE].push(v[0]);
        if (v[5]) access[SYS.HTTP_METHODS.WS].push(v[0]);
    });
    return access;
}

export function getUserRole(access, subscriptions = []) {
    let role = Object.keys(SYS.USER_ROLES).find(k => accessEquals(access || [], SYS.USER_ROLES[k], subscriptions));
    return role || SYS.USER_ROLES.CUSTOM;
}

function accessEquals(access, role, subscriptions = []) {
    let presetAccess = getUserRoleAccess(role, subscriptions),
        methods = Object.keys(SYS.HTTP_METHODS);
    for (let i = 0; i < methods.length; i++) {
        let a1 = access[methods[i]],
            a2 = presetAccess[methods[i]],
            l1 = a1 ? a1.length : 0,
            l2 = a2 ? a2.length : 0;
        if (l1 !== l2) return false;
        if (l1 === 0 && l2 === 0) continue;
        if (!a1.every(v => a2.indexOf(v) !== -1)) return false;
    }
    return true;
}

export function getUserRoleAccess(role, subscriptions = []) {
    let access = {};
    if (role) {
        let resourceTypes = subscriptions.slice();
        // console.log('subscriptions: ', resourceTypes);
        resourceTypes.splice(resourceTypes.indexOf('users'), 1);

        if (role === SYS.USER_ROLES.VIEW) {
            access.GET = resourceTypes;
            access.WS = resourceTypes;
        } else if (role === SYS.USER_ROLES.WORK) {
            access.POST = ['jobs', 'variables'];
            access.GET = resourceTypes;
            access.PUT = ['jobs', 'variables'];
            access.DELETE = ['jobs', 'variables'];
            access.WS = resourceTypes;
        } else if (role === SYS.USER_ROLES.CONFIG) {
            access.POST = resourceTypes;
            access.GET = resourceTypes;
            access.PUT = resourceTypes;
            access.DELETE = resourceTypes;
            access.WS = resourceTypes;
        } else if (role === SYS.USER_ROLES.ADMIN) {
            resourceTypes.push('users');
            access.POST = resourceTypes;
            access.GET = resourceTypes;
            access.PUT = resourceTypes;
            access.DELETE = resourceTypes;
            access.WS = resourceTypes;
        }
    } else {
        // TODO:
        // for (let c = 1; c < this.accessTable.rows[0].cells.length; c++) {
        //     let method = this.accessTable.rows[0].cells[c].innerHTML;
        //     access[method] = [];
        //     for (let r = 1; r < this.accessTable.rows.length; r++) {
        //         if (!this.accessTable.rows[r].cells[c].classList.contains('tableInputCheckboxChecked')) continue;
        //         access[method].push(this.accessTable.rows[r].cells[0].innerHTML.lowercase());
        //     }
        // }
    }
    return access;
}

export function shakeUser(o) {
    let rtn = _.cloneDeep(o);
    // rtn.access = list2access(o.accessListExt);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    return shake(rtn);
}

export function combineUser(os) {
    let o = {
        id: 'newUser',
        access: {
            GET: [],
            PUT: [],
            POST: [],
            DELETE: []
        }
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    combineObjects(os, o, 'properties');
    return o;
}

// variables
export function extendVariables(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendVariable(o));
    return list;
}

export function extendVariable(o) {
    if (!o) return;
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.lockExt = 'icon icon-unlock';
    Object.assign(o, extendVariableAsync(o));
    return o;
}

export function extendVariableAsync(o, variables) {
    if (!o.id || !o.properties) return o;
    if (variables && variables.length) {
        let t = variables.filter(v => v.id === o.id);
        if (!t || !t[0]) return o;
        o.properties = t[0].properties;
        o.value = t[0].value;
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function combineVariables(os) {
    let o = {
        id: 'newVar'
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    combineObjects(os, o, 'properties');
    return o;
}

export function shakeVariable(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    return shake(rtn);
}

// events
export function extendEvents(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendEvent(o));
    return list;
}

export function extendEvent(o) {
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    o.taskListExt = objects2array(o.taskList);
    // async properties...
    o.lockExt = 'icon icon-unlock';
    o.startExt = Timer.specialTime(o.start);
    o.stopExt = Timer.specialTime(o.stop);
    o.isNowExt = 'icon' + (o.isNow === 'true' ? ' icon-clock yellow' : ' icon-subtract');
    Object.assign(o, extendEventAsync(o));
    return o;
}

export function extendEventAsync(o, events) {
    if (!o.id || !o.properties) return o;
    if (events && events.length) {
        let t = events.filter(v => v.id === o.id);
        if (!t || !t[0]) return o;
        o.properties = t[0].properties;
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    o.isNowExt = 'icon' + (o.isNow === 'true' ? ' icon-clock yellow' : ' icon-subtract');
    return o;
}

export function combineEvents(os) {
    let o = {
        id: 'newEvent',
        type: 'SINGLE',
        start: Timer.getTomorrow(),
        stop: Timer.getTomorrow()
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    combineObjects(os, o, 'properties');
    return o;
}

export function shakeEvent(o) {
    let rtn = _.cloneDeep(o);
    rtn.start = new Date(rtn.start).getTime();
    rtn.stop = new Date(rtn.stop).getTime();
    if (rtn.taskListExt && rtn.taskListExt.length > 0)
        rtn.taskList = array2objectArray(rtn.taskListExt, ['trigger', 'method', 'path', 'body']);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    return shake(rtn);
}

// settings
export function extendSettings(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendSetting(o));
    return list;
}

export function extendSetting(o) {
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.lockExt = 'icon icon-unlock';
    Object.assign(o, extendSettingAsync(o));
    return o;
}

export function extendSettingAsync(o, settings) {
    if (!o.id || !o.properties) return o;
    if (settings && settings.length) {
        let t = settings.filter(v => v.id === o.id);
        if (!t || !t[0]) return o;
        o.properties = t[0].properties;
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function combineSettings(os) {
    let o = os[os.length - 1];
    return o;
}

export function shakeSetting(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    return shake(rtn);
}

// scripts
export function extendScripts(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendScript(o));
    return list;
}

export function extendScript(o) {
    /*
     * Q: 'split' of undefined
     * A: 1、o is {}, 2、o.code is undefined, 3、if o.code is null, that 'split' of null
     *              -- used in /src/view/scripts.vue, add by ouyang on 2020.07.22
     */
    if (!o) return;
    if (Object.keys(o).length === 0 || !o.code) o.code = '';

    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    o.linesExt = o.code.split(/\r\n|\r|\n/).length;
    o.loadExt = Timer.getIntervalString(o.load);
    o.intervalExt = Timer.getTimeString(o.interval, true);
    // async properties...
    o.lockExt = 'icon icon-unlock';
    o.startupExt = 'icon icon-subtract';
    o.activeExt = 'icon icon-subtract';
    o.countdownExt = '00:00:00';
    o.errorsExt = '';
    Object.assign(o, extendScriptAsync(o));
    return o;
}

export function extendScriptAsync(o, scripts, timestamp) {
    if (!o.id || !o.properties) return o;
    if (scripts && scripts.length) {
        let t = scripts.filter(v => v.id === o.id);
        if (!t || !t[0]) return o;
        o.properties = _.cloneDeep(t[0].properties);
        // o.load = objects2array(t[0].load);
    }
    o.propertiesExt = objects2array(o.properties);
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    o.loadExt = Timer.getIntervalString(o.load);
    o.startupExt =
        'icon' + (o.properties && o.properties.startup === 'true' ? ' icon-startup yellow' : ' icon-subtract');
    o.activeExt =
        'icon' + (o.properties && o.properties.activation === 'true' ? ' icon-active yellow' : ' icon-subtract');

    let countdown = Math.max(0, o.interval - (timestamp - o.executionTime));
    o.countdownExt = Timer.getTimeString(countdown, true);

    o.errorsExt = o.errorLine !== 0 ? 'Line ' + (o.errorLine < 0 ? '?' : o.errorLine) : '';
    return o;
}

export function combineScripts(os) {
    let o = {
        id: 'newScript',
        type: 'CUSTOM',
        interval: 0,
        code: ''
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    combineObjects(os, o, 'properties');
    return o;
}

export function shakeScript(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.intervalExt) {
        let ts = rtn.intervalExt.split(':'),
            t = 0;
        if (ts && ts.length) {
            t += ts[0] * 3600000;
            t += ts[1] * 60000;
            t += ts[2] * 1000;
        }
        rtn.interval = t;
    }
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    return shake(rtn);
}

// models
export function extendModels(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendModel(o));
    return list;
}

export function extendModel(o) {
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.lockExt = 'icon icon-unlock';
    o.statusExt = [];
    o.errorsExt = [];
    o.actionsExt = [];
    Object.assign(o, extendModelAsync(o));
    return o;
}

export function extendModelAsync(o, models) {
    if (!o.id || !o.properties) return o;
    if (models && models.length) {
        let t = models.filter(v => v.id === o.id);
        if (!t || !t[0]) return o;
        o.properties = _.cloneDeep(t[0].properties);
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function extendModelStatus(o) {
    if (!o || !o.status || typeof o.status !== 'object') return;
    let rtn = [];
    Object.keys(o.status).forEach(k => {
        rtn.push([
            k,
            o.status[k].index,
            o.status[k].length,
            o.status[k].alert,
            o.status[k].actionLink,
            o.status[k].color
        ]);
    });
    o.statusExt = rtn;
    return o;
}

export function extendModelErrors(o) {
    if (!o || !o.errors || typeof o.errors !== 'object') return;
    let rtn = [];
    Object.keys(o.errors).forEach(k => {
        rtn.push([k, o.errors[k].index, o.errors[k].alert]);
    });
    o.errorsExt = rtn;
    return o;
}

export function extendModelActions(o) {
    if (!o || !o.actions || typeof o.actions !== 'object') return;
    let rtn = [];
    Object.keys(o.actions).forEach(k => {
        rtn.push([
            k,
            o.actions[k].code,
            o.actions[k].minValue,
            o.actions[k].maxValue,
            o.actions[k].minTime,
            o.actions[k].maxTime,
            o.actions[k].samples
        ]);
    });
    o.actionsExt = rtn;
    return o;
}

export function extendModelProperties(o) {
    o.propertiesExt = objects2array(o.properties);
    return o;
}

export function combineModels(os) {
    let o = {
        id: 'newModel',
        size: { x: 0, y: 0 },
        offsetOdometry: { x: 0, y: 0 },
        offsetProgress: { x: 0, y: 0 },
        offsetPivot: { x: 0, y: 0 }
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    combineObjects(os, o, 'properties');
    return o;
}

export function shakeModel(o) {
    // let rtn = _.cloneDeep(o);
    let rtn = JSON.parse(JSON.stringify(o));
    console.log('[Chef] rtn: ', rtn.statusExt);
    if (rtn.statusExt && rtn.statusExt.length > 0)
        rtn.status = array2object(rtn.statusExt, ['name', 'index', 'length', 'alert', 'actionLink', 'color']);
    if (rtn.errorsExt && rtn.errorsExt.length > 0) rtn.errors = array2object(rtn.errorsExt, ['name', 'index', 'alert']);
    if (rtn.actionsExt && rtn.actionsExt.length > 0)
        rtn.actions = array2object(rtn.actionsExt, [
            'name',
            'code',
            'minValue',
            'maxValue',
            'minTime',
            'maxTime',
            'samples'
        ]);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    return shake(rtn);
}

export function getModelsOptions(models) {
    let ops = [];
    if (!models || !models.length) return ops;
    models.forEach(s => {
        ops.push({
            label: s.vehicleKey,
            value: s.vehicleKey
        });
    });
    ops.sort((a, b) => a.value - b.value);
    return ops;
}

// vehicles
export function extendVehicles(list, models = []) {
    if (!list || !list.length) return;
    list.forEach(o => extendVehicle(o, models));
    return list;
}

export function extendVehicle(o) {
    /*
     * Q: sentry: isOnline of undefined
     * A: 1、The value o does not passed in, 2、The value o does passed in, but o of undefined
     *              -- used in p-vehicle-detail-form.vue, mod by ouyang on 2020.07.17
     */
    if (!o) return;

    //eslint-disable-line
    o.onlineExt =
        'icon' +
        (o.isOnline
            ? o.isValidated && o.updateIndex < 0
                ? ' icon-wifi green'
                : ' icon-wifi yellow'
            : ' icon-wifi red');
    o.alertExt = 'icon' + (o.hasAlert ? ' icon-alert red' : ' icon-subtract');
    o.stopExt = 'icon' + (o.properties && o.properties.stop === 'true' ? ' icon-stop red' : ' icon-subtract');
    o.remoteExt = 'icon' + (o.remoteId === 'FREE' ? ' icon-subtract' : ' icon-remote yellow');
    o.simulateExt = 'icon' + (o.isSimulated === true ? ' icon-simulate blue' : ' icon-subtract');

    // o.responseExt = Timer.getIntervalString(Math.max(0, serverTime - o.lastTelegramTime));
    let updateProgress = Math.round((o.updateIndex / o.updateLength) * 100);
    o.updateExt = {
        value: o.updateIndex < 0 ? 'NONE' : updateProgress + ' %',
        bg: o.updateIndex < 0 ? null : 'hsla(' + updateProgress + ', 100%, 50%, 0.5)'
    };
    o.mileageExt = Expert.round22(o.mileage / 1000);
    // o.jobtimeExt = Timer.getTimeString(Math.abs(workTime - o.lastJobTime), true);

    // o.modelExt = models.find(m => new RegExp('^' + m.vehicleKey).test(o.id));

    o.firstTelegramTimeExt = Timer.specialTime(o.firstTelegramTime);
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.lockExt = 'icon icon-unlock';
    o.batteryExt = '';
    if (!o.labelOffset) o.labelOffset = { x: 0, y: 0 };
    if (o.properties.pathId) o.pathId = o.properties.pathId;
    Object.assign(o, extendVehicleAsync(o));
    Object.assign(o, extendVehicleBatteryExt(o));
    return o;
}

export function extendVehicleAsync(o, vehicles) {
    if (vehicles && vehicles.length) {
        let t = vehicles.filter(v => v.id === o.id);
        if (!t || !t[0]) return;
        o.lastTelegramTime = t[0].lastTelegramTime;
        o.responseExt = t[0].responseExt;
        o.jobtimeExt = t[0].jobtimeExt;
        o.statusExt = t[0].statusExt;
        o.locationExt = t[0].locationExt;
        o.destinationExt = t[0].destinationExt;
        o.remoteId = t[0].remoteId;
        o.isOnline = t[0].isOnline;
        o.isValidated = t[0].isValidated;
        o.updateIndex = t[0].updateIndex;
        o.properties = t[0].properties;
        o.navigatorId = t[0].navigatorId;
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    o.onlineExt =
        'icon' +
        (o.isOnline
            ? o.isValidated && o.updateIndex < 0
                ? ' icon-wifi green'
                : ' icon-wifi yellow'
            : ' icon-wifi red');
    o.alertExt = 'icon' + (o.hasAlert ? ' icon-alert red' : ' icon-subtract');
    o.stopExt = 'icon' + (o.properties && o.properties.stop === 'true' ? ' icon-stop red' : ' icon-subtract');
    o.remoteExt = 'icon' + (o.remoteId === 'FREE' ? ' icon-subtract' : ' icon-remote yellow');
    o.simulateExt = 'icon' + (o.isSimulated === true ? ' icon-simulate blue' : ' icon-subtract');

    // o.responseExt = Timer.getIntervalString(Math.max(0, vueIns.$root.ws.serverTime - o.lastTelegramTime));
    let updateProgress = Math.round((o.updateIndex / o.updateLength) * 100);
    o.updateExt = {
        value: !o.updateIndex || o.updateIndex < 0 ? 'NONE' : updateProgress + ' %',
        bg: o.updateIndex < 0 ? null : 'hsla(' + updateProgress + ', 100%, 50%, 0.5)'
    };
    // o.updateExt = !o.updateIndex || o.updateIndex < 0 ? 'NONE' : updateProgress + ' %';

    o.mileageExt = Expert.round22(o.mileage / 1000);
    Object.assign(o, extendVehicleBatteryExt(o));

    return o;
}

export function extendVehicleBatteryExt(o) {
    if (!o || !o.statusExt) return;
    let b;
    if (o.statusExt['Energy [%]']) b = parseInt(o.statusExt['Energy [%]'].value);
    else if (o.statusExt['Energy']) b = parseInt(o.statusExt['Energy'].value);
    else if (o.statusExt['Batterie']) b = parseInt(o.statusExt['Batterie'].value);
    else if (o.statusExt['Battery']) b = parseInt(o.statusExt['Battery'].value);

    if (isNaN(b)) o.batteryExt = 'icon-battery';
    else if (b > 90) o.batteryExt = 'icon-battery-fill green';
    else if (b > 80) o.batteryExt = 'icon-battery-80 green';
    else if (b > 60) o.batteryExt = 'icon-battery-60 green';
    else if (b > 40) o.batteryExt = 'icon-battery-40 yellow';
    else if (b > 20) o.batteryExt = 'icon-battery-20 red';
    else o.batteryExt = 'icon-battery red';

    return o;
}

export function extendVehicleErrors(o, vehicles, models = [], serverTime = Date.now()) {
    if (!models || !models.length) return;
    if (vehicles && vehicles.length) {
        let t = vehicles.filter(v => v.id === o.id);
        if (!t || !t[0]) return;
        o.errorHistory = t[0].errorHistory;
    }
    let model = models.find(m => new RegExp('^' + m.vehicleKey).test(o.id)),
        errors = [];
    if (!o || !model || !model.errors) return o;

    for (let key in model.errors) {
        if (!model.errors.hasOwnProperty(key)) continue;
        if (!o.errorHistory) o.errorHistory = {};

        let error = model.errors[key],
            h = (o.errorHistory ? o.errorHistory[key] : {}) || {},
            isSet = error && error.index < 0 ? o.properties[key] === 'true' : Expert.checkBit(o.rawInput, error.index);
        // console.log('----------> ', o.id, key, error, o.rawInput);
        if (!isSet && !h.time) continue;

        if (isSet !== h.state) {
            h.state = isSet;
            h.time = serverTime;
        }
        o.errorHistory[key] = h;

        errors.push({
            datetime: Timer.getDateTimeString(h.time),
            msg: key
        });
    }
    o.errorsExt = errors;
    return o;
}

// export function extendVehicleIO(o) {
//     let rtn = [],
//         len = Math.max(o.rawInput.length, o.rawOutput.length);
//     for (let i = 0; i < len; i++) {
//         let rawInput = { inputIdx: '', inputASC: '', inputHEX: '', inputDEC: '', inputBIN: '' },
//             rawOutput = { outputIdx: '', outputASC: '', outputHEX: '', outputDEC: '', outputBIN: '' };
//         if (o.rawInput[i]) {
//             rawInput.inputIdx = i;
//             rawInput.inputASC = String.fromCharCode(o.rawInput[i] & 0xff);
//             rawInput.inputHEX = Expert.getHexString(o.rawInput[i] & 0xff);
//             rawInput.inputDEC = o.rawInput[i] & 0xff;
//             rawInput.inputBIN = Expert.getBinaryString(o.rawInput[i] & 0xff);
//         }
//         if (o.rawOutput[i]) {
//             rawOutput.outputIdx = i;
//             rawOutput.outputASC = String.fromCharCode(o.rawOutput[i] & 0xff);
//             rawOutput.outputHEX = Expert.getHexString(o.rawOutput[i] & 0xff);
//             rawOutput.outputDEC = o.rawOutput[i] & 0xff;
//             rawOutput.outputBIN = Expert.getBinaryString(o.rawOutput[i] & 0xff);
//         }
//         rtn.push({ ...rawInput, ...rawOutput });
//     }
//     o.ioExt = rtn;
//     return o;
// }
export function extendVehicleIO(o, vehicles) {
    if (!o || !o.rawInput || !o.rawOutput) return;

    /*
     * Q: sentry: length of undefined
     * A: 1、o === {}, 2、o.rawInput === undefined <-- D: put || []
     *              -- used in p-vehicle-detail-form.vue, mod by ouyang on 2020.7.17
     */
    if (vehicles && vehicles.length) {
        let t = vehicles.filter(v => v.id === o.id);
        if (t && t[0]) {
            o.rawInput = t[0].rawInput || [];
            o.rawOutput = t[0].rawOutput || [];
        }
    }
    /**
     * 把rawInput 和 rawOutput 数据分辨整理城rawInput 、rawOutput
     * 获取rawInput、rawOutput最大长度 进行for循环
     * 判断rawInput数量大于rawOutput数量
     * @type {*[]}
     */

    let rtn = [],
        len = Math.max(o.rawInput.length, o.rawOutput.length),
        rawInput = ['', '', '', '', ''],
        rawOutput = ['', '', '', '', ''],
        inRtn = [],
        outRtn = [];
    for (let i = 0; i < o.rawInput.length; i++) {
        rawInput = [
            i,
            String.fromCharCode(o.rawInput[i] & 0xff),
            Expert.getHexString(o.rawInput[i] & 0xff),
            o.rawInput[i] & 0xff,
            Expert.getBinaryString(o.rawInput[i] & 0xff)
        ];
        inRtn.push(rawInput);
    }
    for (let i = 0; i < o.rawOutput.length; i++) {
        rawOutput = [
            i,
            String.fromCharCode(o.rawOutput[i] & 0xff),
            Expert.getHexString(o.rawOutput[i] & 0xff),
            o.rawOutput[i] & 0xff,
            Expert.getBinaryString(o.rawOutput[i] & 0xff)
        ];
        outRtn.push(rawOutput);
    }
    for (let i = 0; i < len; i++) {
        if (o.rawInput.length > o.rawOutput.length) {
            if (outRtn[i]) {
                if (inRtn[i][0] === outRtn[i][0]) {
                    rtn.push(inRtn[i].concat(outRtn[i]));
                }
            } else {
                rtn.push(inRtn[i].concat(['', '', '', '', '']));
            }
        } else {
            if (inRtn[i]) {
                if (outRtn[i][0] === inRtn[i][0]) {
                    rtn.push(inRtn[i].concat(outRtn[i]));
                }
            } else {
                rtn.push(['', '', '', '', ''].concat(outRtn[i]));
            }
        }
    }
    // let rtn = [],
    //     len = Math.max(o.rawInput.length, o.rawOutput.length);
    // for (let i = 0; i < len; i++) {
    //     let rawInput = ['', '', '', '', ''],
    //         rawOutput = ['', '', '', '', ''];
    //     if (o.rawInput[i]) {
    //         rawInput = [
    //             i,
    //             String.fromCharCode(o.rawInput[i] & 0xff),
    //             Expert.getHexString(o.rawInput[i] & 0xff),
    //             o.rawInput[i] & 0xff,
    //             Expert.getBinaryString(o.rawInput[i] & 0xff)
    //         ];
    //     }
    //     if (o.rawOutput[i]) {
    //         rawOutput = [
    //             i,
    //             String.fromCharCode(o.rawOutput[i] & 0xff),
    //             Expert.getHexString(o.rawOutput[i] & 0xff),
    //             o.rawOutput[i] & 0xff,
    //             Expert.getBinaryString(o.rawOutput[i] & 0xff)
    //         ];
    //     }
    //     rtn.push([...rawInput, ...rawOutput]);
    // }
    o.ioExt = Object.freeze(rtn);
    return o;
}

export function shakeVehicle(o) {
    let rtn = JSON.parse(JSON.stringify(o));
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    rtn = shake(rtn);
    return rtn;
}

export function combineVehicle(os) {
    let o = os[os.length - 1];
    return o;
}

export function getVehicleModelById(id, models) {
    if (!id || !models || !models.length) return;
    let m = (models || []).find(m => m.vehicleKey && new RegExp('^' + m.vehicleKey).test(id));
    return m;
}

export function getVehicleCurrentRoutesById(id, vehicles) {
    if (!id || !vehicles) return [];
    let o = vehicles.find(t => t.id === id);
    return (o && o.route) || [];
}

export function getVehicleIdByJobId(id, jobs) {
    if (!id || !jobs) return '';
    let o = jobs.find(t => t.id === id);
    return (o && o.vehicleId) || '';
}

export function getVehicleByPosition(p, vehicles) {
    let rtn = null;
    if (!p || !vehicles || !vehicles.length) return rtn;
    rtn = vehicles.find(v => {
        if (!v.tunedPos || !v.modelExt) return false;
        let s = v.modelExt.size,
            r = Math.max(s.x, s.y) / 2;
        // console.info('-> ', v.id, v.tunedPos, r, p);
        return Expert.checkPointInCircle(v.tunedPos.x, v.tunedPos.y, r, p.x, p.y);
    });
    return rtn;
}

// jobs
export function extendJobs(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendJob(o));
    return list;
}

export function extendJob(o, models) {
    /**
     * 当指派时间是0时，指派时间扩展为空bug6061
     */
    if (o.appointmentTime === 0) o.appointmentTimeExt = '';
    else o.appointmentTimeExt = Timer.getDateTimeString(o.appointmentTime);
    o.priorityExt = {
        value: o.priority,
        bg: 'rgba(255,0,0,' + Expert.round22(0.5 - o.priority / 200) + ')'
    };
    o.vehicleKeyExt = o.vehicleKey || 'NONE';
    o.assignmentExt = o.vehicleId || 'NONE';
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    o.orderListExt = objects2array(o.orderList);
    // (o.orderListExt || []).forEach((v, i) => (v.isAppointment = i === o.appointmentIndex ? 1 : 0));
    (o.orderListExt || []).forEach((v, i) => v.unshift(i === o.appointmentIndex ? 1 : 0));
    if (!o.appointmentModeExt) o.appointmentModeExt = o.appointmentMode;
    // async properties...
    o.lockExt = 'icon icon-unlock';
    Object.assign(o, extendJobAsync(o));
    o.modelExt = getVehicleModelById(o.vehicleId, models);
    return o;
}

export function extendJobAsync(o, jobs) {
    if (!o.id || !o.properties) return;
    if (jobs && jobs.length) {
        let t = jobs.filter(v => v.id === o.id);
        if (!t || !t[0]) return;
        o.orderIndex = t[0].orderIndex;
        o.progress = t[0].progress;
        o.progressLabelExt = t[0].progressLabelExt;
        o.progressColorExt = t[0].progressColorExt;
        o.label = t[0].label;
        o.deviation = t[0].deviation;
        o.properties = t[0].properties;
        o.assignmentExt = t[0].vehicleId || 'NONE';
        o.initTimeExt = Timer.specialTime(o.initTime);
        if (t[0].appointmentTime === 0) o.appointmentTimeExt = '';
        else o.appointmentTimeExt = Timer.getDateTimeString(t[0].appointmentTime);
        if (o.priority !== t[0].priority) {
            o.priority = t[0].priority;
            o.priorityExt = {
                value: t[0].priority,
                bg: 'rgba(255,0,0,' + Expert.round22(0.5 - t[0].priority / 200) + ')'
            };
        }
    }

    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');

    o.deviationExt = Timer.getIntervalString(o.deviation, true);
    // status color
    o.jobStatusColorExt = SYS.STATUS_COLORS.GRAY;
    // status's describe
    o.jobStatusDescExt = '';

    /*
     * Q: sentry: status of undefined
     * A: 1、o = {}, 2、o.properties = undefined
     *              -- used in p-job-detail-form.vue, mod by ouyang on 2020.07.17
     */
    if ((o && Object.keys(o).length === 0) || o.properties === undefined) o.properties = {};

    if (o.properties.status === SYS.JOB_STATUS.COMPLETED) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusCompleted');
        o.jobStatusDescExt = 'Job completed';
    } else if (o.properties.status === SYS.JOB_STATUS.DEFERRED) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusDeferred');
        o.jobStatusDescExt = 'Vehicle already at destination / no action required';
    } else if (o.properties.status === SYS.JOB_STATUS.NO_ROUTE) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusNoRoute');
        o.jobStatusDescExt = 'Can not find route for current order';
        o.jobStatusColorExt = SYS.STATUS_COLORS.RED;
    } else if (o.properties.status === SYS.JOB_STATUS.NO_VEHICLE) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusNoVehicle');
        o.jobStatusDescExt = 'No vehicle available for key or route';
        o.jobStatusColorExt = SYS.STATUS_COLORS.RED;
    } else if (o.properties.status === SYS.JOB_STATUS.SCHEDULED) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusScheduled');
        o.jobStatusDescExt = 'Job scheduled...';
    } else if (o.properties.status === SYS.JOB_STATUS.ASSIGNED) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusAssigned').format(o.vehicleId);
        o.jobStatusDescExt = `Vehicle assigned: ${o.vehicleId}`;
        o.jobStatusColorExt = SYS.STATUS_COLORS.GREEN;
    } else if (o.properties.status === SYS.JOB_STATUS.DELAYED) {
        // o.jobStatusDescExt = i18n.t('lang.jobStatusDelayed').format(Timer.getIntervalString(o.deviation));
        o.jobStatusDescExt = `Appointment delayed by ${Timer.getIntervalString(o.deviation)}`;
        o.jobStatusColorExt = SYS.STATUS_COLORS.YELLOW;
    }
    // status icon
    o.jobStatusExt = 'icon icon-subtract';
    if (o.properties.status !== SYS.JOB_STATUS.NO_ROUTE) o.jobStatusExt = 'icon icon-status-fill';
    o.jobStatusExt += ' ' + o.jobStatusColorExt;
    o.jobLoopExt = 'icon' + (o.endEvent === 'RESTART' ? ' icon-loop blue' : ' icon-subtract');

    // let appointmentDuration = Math.max(1, o.appointmentTime - o.startTime),
    //     deviationFactor = Math.min(0.5, Math.max(0, o.deviation / (2 * appointmentDuration)));
    o.progressExt = {
        // value: Expert.round22(o.progress * 100) + ' %',
        // bg: 'rgba(255,0,0,' + Expert.round22(deviationFactor) + ')'
        value: o.progressLabelExt + ' %',
        bg: o.progressColorExt
    };
    return o;
}

export function combineJobs(os) {
    let o = {
        id: -1,
        vehicleKey: '',
        priority: 10,
        endEvent: 'REMOVE',
        appointmentMode: 'ASAP',
        appointmentIndex: 0,
        appointmentTime: Date.now(),
        orderList: [{ path: -1, range: -1, actionCode: -1, actionValue: 0 }]
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    os.forEach(v => {
        if (v.id !== o.id) o.id = '?';
    });
    combineObjects(os, o, 'properties');
    return o;
}

export function separateJobs(jobs, obj) {
    return obj;
}

export function shakeJob(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    // rtn.orderList = array2objectArray(rtn.orderListExt, ['path', 'actionCode', 'actionValue']);
    let orderList = rtn.orderListExt;
    (rtn.orderListExt || []).forEach((v, i) => {
        let p = v.shift();
        if (p) rtn.appointmentIndex = i;
    });
    if (!rtn.appointmentIndex || rtn.appointmentIndex < 0) rtn.appointmentIndex = 0;
    console.log('[Chef] shakeJob: ', orderList);
    if (orderList && orderList.length > 0)
        rtn.orderList = array2objectArray(orderList, ['path', 'range', 'actionCode', 'actionValue']);
    rtn.appointmentTime = new Date(rtn.appointmentTimeExt).getTime();
    rtn = shake(rtn);
    console.log('[Chef] shakeJob: ', o, rtn);
    return rtn;
}

export function getJobProgressById(id, jobs = []) {
    if (!id || !jobs) return '';
    let o = jobs.find(t => t.id === id);
    return (o && o.progress && Expert.round22(o.progress * 100) + ' %') || '';
}

// contours
export function extendContours(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendContour(o));
    return list;
}

export function extendContour(o) {
    o.labelExt = o.label ? { value: o.label, bg: o.labelColor } : null;
    // o.pointsExt = (o.points && o.points.length) || 0;
    o.pointsExt = objects2array(o.points);
    o.pointsLenExt = o.points ? parseInt(o.points.length) : 0;
    o.areaExt = o.area ? o.area.toFixed(2) : o.area;
    // o.colorExt = { value: Timer.rgbaToHexString(o.color), bg: o.color };
    o.colorExt = { value: o.color, bg: o.color };
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.actionExt = SYS.PAINTER_ACTIONS.NONE;
    o.actionXExt = 0;
    o.actionYExt = 0;
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function extendContourAsync(o, contours) {
    if (!o.id || !o.properties) return;
    if (contours && contours.length) {
        let t = contours.filter(v => v.id === o.id);
        if (!t || !t[0]) return;
        o.properties = t[0].properties;
        o.layer = t[0].layer;
        if (o.label !== t[0].label || o.labelColor !== t[0].labelColor) {
            o.label = t[0].label;
            o.labelExt = {
                bg: t[0].labelColor,
                value: t[0].label
            };
            // o.labelExt['bg'] = t[0].labelColor;
            // o.labelExt['value'] = t[0].label;
        }
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function shakeContour(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.pointsExt) rtn.points = array2objectArray(rtn.pointsExt, ['x', 'y']);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    rtn = shake(rtn);
    // console.warn('--2--> ', rtn);
    return rtn;
}

export function combineContours(os) {
    if (!os || !os.length) return {};
    let o = _.cloneDeep(os[os.length - 1]);
    os.forEach(v => {
        if (v.id !== o.id) o.id = '?';
        if (v.color !== o.color) o.color = '?';
        if (v.layer !== o.layer) o.layer = '?';
        if (v.label !== o.label) o.label = '?';
        if (v.labelColor !== o.labelColor) o.labelColor = '?';
        if (v.labelOffset.x !== o.labelOffset.x) o.labelOffset.x = '?';
        if (v.labelOffset.y !== o.labelOffset.y) o.labelOffset.y = '?';

        if (v.m3d !== o.m3d) o.m3d = '?';
        if (v.m3dScale !== o.m3dScale) o.m3dScale = '?';
        if (o.m3dOffset && v.m3dOffset.x !== o.m3dOffset.x) o.m3dOffset.x = '?';
        if (o.m3dOffset && v.m3dOffset.y !== o.m3dOffset.y) o.m3dOffset.y = '?';
    });
    combineObjects(os, o, 'properties');
    combineArray(os, o, 'points');
    return o;
}

export function separateContours(contours, obj) {
    if (!contours || !contours.length) return;
    let os = _.cloneDeep(contours);
    let rtn = [];
    os.forEach(o => {
        if (obj && obj.id !== '?') o.id = Math.floor(obj.id);
        if (obj && obj.color !== '?') o.color = obj.color;
        if (obj && obj.layer !== '?') o.layer = Math.floor(obj.layer);
        if (obj && obj.label !== '?') o.label = obj.label;
        if (obj && obj.labelColor !== '?') o.labelColor = obj.labelColor;
        if (obj && obj.labelOffset.x !== '?') o.labelOffset.x = obj.labelOffset.x;
        if (obj && obj.labelOffset.y !== '?') o.labelOffset.y = obj.labelOffset.y;
        if (obj && obj.m3d !== '?') o.m3d = obj.m3d;
        if (obj && obj.m3dScale !== '?') o.m3dScale = obj.m3dScale;
        if (obj && obj.m3dOffset.x !== '?') o.m3dOffset.x = obj.m3dOffset.x;
        if (obj && obj.m3dOffset.y !== '?') o.m3dOffset.y = obj.m3dOffset.y;
        if (obj && obj.m3dOffset.z !== '?') o.m3dOffset.z = obj.m3dOffset.z;
        if (obj && obj.m3dOffset.h !== '?') o.m3dOffset.h = obj.m3dOffset.h;
        if (obj && obj.m3dHeight !== '?') o.m3dHeight = obj.m3dHeight;
        // if (obj && obj.points && obj.points.length) o.points = _.cloneDeep(obj.points);
        // separateObject(obj, o, 'points');
        separateObject(obj, o, 'properties');
        separateArray(obj, o, 'points');
        rtn.push(shakeContour(o));
    });
    return rtn;
}

// ranges
export function extendRanges(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendRange(o));
    return list;
}

export function extendRange(o) {
    o.labelExt = o.label ? { value: o.label, bg: o.labelColor } : null;
    // o.pointsExt = (o.points && o.points.length) || 0;
    o.pointsExt = objects2array(o.points);
    o.pointsLenExt = o.points ? parseInt(o.points.length) : 0;
    o.areaExt = o.area ? o.area.toFixed(2) : o.area;
    // o.colorExt = { value: Timer.rgbaToHexString(o.color), bg: o.color };
    o.colorExt = { value: o.color, bg: o.color };
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.actionExt = SYS.PAINTER_ACTIONS.NONE;
    o.actionXExt = 0;
    o.actionYExt = 0;
    o.propertiesExt = objects2array(o.properties);
    // async properties...
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function extendRangeAsync(o, ranges) {
    if (!o.id || !o.properties) return;
    if (ranges && ranges.length) {
        let t = ranges.filter(v => v.id === o.id);
        if (!t || !t[0]) return;
        o.properties = t[0].properties;
        o.layer = t[0].layer;
        if (o.label !== t[0].label || o.labelColor !== t[0].labelColor) {
            o.label = t[0].label;
            o.labelExt = {
                bg: t[0].labelColor,
                value: t[0].label
            };
        }
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function shakeRange(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.pointsExt) rtn.points = array2objectArray(rtn.pointsExt, ['x', 'y']);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);
    rtn = shake(rtn);
    return rtn;
}

export function combineRanges(os) {
    if (!os || !os.length) return {};
    let o = _.cloneDeep(os[os.length - 1]);
    os.forEach(v => {
        if (v.id !== o.id) o.id = '?';
        if (v.color !== o.color) o.color = '?';
        if (v.layer !== o.layer) o.layer = '?';
        if (v.label !== o.label) o.label = '?';
        if (v.labelColor !== o.labelColor) o.labelColor = '?';
        if (v.labelOffset.x !== o.labelOffset.x) o.labelOffset.x = '?';
        if (v.labelOffset.y !== o.labelOffset.y) o.labelOffset.y = '?';
    });
    combineObjects(os, o, 'properties');
    combineArray(os, o, 'points');
    return o;
}

export function separateRanges(ranges, obj) {
    if (!ranges || !ranges.length) return;
    let os = _.cloneDeep(ranges);
    os.forEach(o => {
        if (obj && obj.id !== '?') o.id = Math.floor(obj.id);
        if (obj && obj.color !== '?') o.color = obj.color;
        if (obj && obj.layer !== '?') o.layer = Math.floor(obj.layer);
        if (obj && obj.label !== '?') o.label = obj.label;
        if (obj && obj.labelColor !== '?') o.labelColor = obj.labelColor;
        if (obj && obj.labelOffset.x !== '?') o.labelOffset.x = obj.labelOffset.x;
        if (obj && obj.labelOffset.y !== '?') o.labelOffset.y = obj.labelOffset.y;
        // if (obj && obj.points && obj.points.length) o.points = _.cloneDeep(obj.points);
        separateObject(obj, o, 'properties');
    });
    return os;
}

export function getRangesOptions(ranges) {
    let ops = [];
    if (!ranges || !ranges.length) return ops;
    ranges.forEach(s => {
        ops.push({
            label: s.label && s.label !== s.id ? `${s.id} - ${s.label}` : s.id,
            value: s.id
        });
    });
    ops.sort((a, b) => a.value - b.value);
    return ops;
}

// paths
export function extendPaths(list) {
    if (!list || !list.length) return;
    list.forEach(o => extendPath(o));
    return list;
}

export function extendPath(o) {
    /*
     * Q: sentry: absoluteSpeed of undefined
     * A: 1、o === {}, 2、o.properties === undefined
     *              -- used in p-path-detail-form.vue, mod by ouyang on 2020.7.15
     */
    if ((o && Object.keys(o).length === 0) || o.properties === undefined) o.properties = {};
    let absoluteSpeed = o.properties.absoluteSpeed ? o.properties.absoluteSpeed : ''; // if absoluteSpeed is null or undefined, it's empty

    o.colorExt = { value: o.color, bg: o.color };
    o.labelExt = o.label ? { value: o.label, bg: o.labelColor } : null;
    if (o.controlPoints && o.controlPoints.length) o.partsExt = Math.max(1, (o.controlPoints.length - 1) / 3);
    else o.partsExt = 0;
    o.lengthExt = Expert.round22(o.length / 1000);
    o.absoluteSpeedExt = absoluteSpeed + (isNaN(o.properties.absoluteSpeed) ? '' : ' m/s');
    o.relativeSpeedExt = {
        value: (Number(o.properties.relativeSpeed) || 100) + '%',
        bg: 'rgba(255,0,0,' + Expert.round22(1 - o.properties.relativeSpeed / 100) + ')'
    };
    let condition = Number(o.properties.condition) || 100;
    o.conditionExt = {
        value: condition + '%',
        bg: 'rgba(255,215,0,' + Expert.round22(1 - condition / 100) + ')'
    };
    o.spotTurnExt = 'icon' + (o.properties.spotTurn === 'true' ? ' icon-turn blue' : ' icon-subtract');
    o.closureExt = 'icon' + (o.properties.closure === 'true' ? ' icon-closure red' : ' icon-subtract');
    o.stopSignExt =
        'icon' + (o.properties.stop && Number(o.properties.stop) !== -1 ? ' icon-stop red' : ' icon-subtract');
    // o.partsExt = Math.max(1, (o.controlPoints.length - 1) / 3);
    o.absoluteSpeedExt = absoluteSpeed + (isNaN(o.properties.absoluteSpeed) ? '' : ' m/s');
    o.relativeSpeedExt = {
        value: (Number(o.properties.relativeSpeed) || 100) + '%',
        bg: 'rgba(255,0,0,' + Expert.round22(1 - o.properties.relativeSpeed / 100) + ')'
    };
    o.initTimeExt = Timer.specialTime(o.initTime);
    o.modificationTimeExt = Timer.specialTime(o.modificationTime);
    o.propertiesExt = objects2array(o.properties);
    if (o.controlPoints && o.controlPoints.length) {
        o.headingStartExt = o.controlPoints[0].h;
        o.headingEndExt = o.controlPoints[o.controlPoints.length - 1].h;
    }
    o.actionExt = SYS.PAINTER_ACTIONS.NONE;
    o.actionXExt = 0;
    o.actionYExt = 0;
    // async properties...
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    Object.assign(o, extendPathAsync(o));
    return o;
}

export function extendPathAsync(o, paths) {
    if (!o.id || !o.properties) return;
    if (paths && paths.length) {
        let t = paths.filter(v => v.id === o.id);
        if (!t || !t[0]) return;
        // console.log(JSON.stringify(o.properties));
        o.properties = Object.freeze(_.cloneDeep(t[0].properties));
    }
    o.lockExt = 'icon' + (o.properties && o.properties.lock === 'true' ? ' icon-lock yellow' : ' icon-unlock');
    return o;
}

export function combinePath(os) {
    let o = {
        id: '',
        actionExt: SYS.PAINTER_ACTIONS.NONE
    };
    if (!os || !os.length) return o;

    o = _.cloneDeep(os[os.length - 1]);
    os.forEach(v => {
        if (v.id !== o.id) o.id = '?';
        if (v.width !== o.width) o.width = '?';
        if (v.color !== o.color) o.color = '?';
        if (v.label !== o.label) o.label = '?';
        if (v.labelColor !== o.labelColor) o.labelColor = '?';
        if (v.labelOffset.x !== o.labelOffset.x) o.labelOffset.x = '?';
        if (v.labelOffset.y !== o.labelOffset.y) o.labelOffset.y = '?';
        if (v.heading !== o.heading) o.heading = '?';
        if (v.controlPoints[0].h !== o.controlPoints[0].h) o.controlPoints[0].h = '?';
        if (v.controlPoints[v.controlPoints.length - 1].h !== o.controlPoints[o.controlPoints.length - 1].h)
            o.controlPoints[o.controlPoints.length - 1].h = '?';
    });
    combineObjects(os, o, 'properties');
    return o;
}
export function separatePaths(paths, obj, bezier) {
    if (!paths || !paths.length) return;
    let rtn = [];
    paths.forEach(o => {
        if (obj && obj.id !== '?') o.id = Math.floor(obj.id);
        if (obj && obj.width !== '?') o.width = obj.width;
        if (obj && obj.color !== '?') o.color = obj.color;
        if (obj && obj.label !== '?') o.label = obj.label;
        if (obj && obj.labelColor !== '?') o.labelColor = obj.labelColor;
        if (obj && obj.labelOffset.x !== '?') o.labelOffset.x = obj.labelOffset.x;
        if (obj && obj.labelOffset.y !== '?') o.labelOffset.y = obj.labelOffset.y;
        if (obj && obj.heading !== '?') o.heading = obj.heading;
        // if (obj && obj.controlPoints[0].h !== '?') o.controlPoints[0].h = obj.controlPoints[0].h;
        // if (obj && obj.controlPoints[obj.controlPoints.length - 1].h !== '?')
        //     o.controlPoints[o.controlPoints.length - 1].h = obj.controlPoints[obj.controlPoints.length - 1].h;
        if (bezier && obj && obj.controlPoints && obj.controlPoints.length) {
            o.controlPoints = _.cloneDeep(obj.controlPoints);
        }
        separateObject(obj, o, 'properties');
        rtn.push(shakePath(o));
    });
    return rtn;
}

export function setControlPoints(path) {
    let x = Number(path.actionXExt);
    let y = Number(path.actionYExt);
    let i = 0;

    switch (path.actionExt) {
        case 'REVERSE':
            path.controlPoints.reverse();
            break;

        case 'MOVE':
            for (; i < path.controlPoints.length; i++) {
                path.controlPoints[i].x += x;
                path.controlPoints[i].y += y;
            }
            break;

        case 'MIRROR X':
            for (; i < path.controlPoints.length; i++) {
                path.controlPoints[i].x = 2 * x - path.controlPoints[i].x;
            }
            break;

        case 'MIRROR Y':
            for (; i < path.controlPoints.length; i++) {
                path.controlPoints[i].y = 2 * y - path.controlPoints[i].y;
            }
            break;
    }
    return path.controlPoints;
}

export function shakePath(o) {
    let rtn = _.cloneDeep(o);
    if (rtn.propertiesExt && rtn.propertiesExt.length > 0)
        rtn.properties = array2object(rtn.propertiesExt, ['name', 'value']);

    rtn = shake(rtn);
    return rtn;
}

export function getPathsOptions(paths) {
    let ops = [];
    if (!paths || !paths.length) return ops;
    paths.forEach(s => {
        ops.push({
            label: s.label && s.label !== s.id ? `${s.id} - ${s.label}` : s.id,
            value: s.id
        });
    });
    ops.sort((a, b) => a.value - b.value);
    return ops;
}

export function getPathByPoint(p, paths) {
    let rtn = null;
    if (!p || !paths || !paths.length) return rtn;
    rtn = paths.find(v => {
        let sp = v.controlPoints[v.controlPoints.length - 1],
            dis = new Point(p.x, p.y).distance(sp);
        return dis <= 1000;
    });
    return rtn;
    // let distance = new Point(p.x, p.y).distance()
}

export const findKey = (value, obj, compare = (a, b) => a === b) => {
    return Object.keys(obj).find(k => compare(obj[k], value));
};

// logs
export function extendLogs(list) {
    if (!list || !list.length) return;
    let rtn = [];
    list.forEach((v, i) => rtn.push(extendLog(v, i)));
    return rtn;
}

export function extendLog(v, i) {
    let o = {};
    o.id = i || Date.now();
    o.name = 'server_' + i + '.log';
    o.modified = Timer.specialTime(v);
    return o;
}
